package both.map.mapModel;

import java.util.HashMap;
import java.util.Map;
import both.map.LayerType;
import java.awt.Dimension;

/**
 * @author jdl
 */
public class ModelChunk {
	private Dimension size;
	
	private Map<LayerType, ModelLayer> layers;
	
	/**
	 * build a new ModelChunk, with empty layers.
	 * @param size the size of this chunk
	 */
	public ModelChunk(Dimension size)
	{
		this.size = size;
		layers = new HashMap<LayerType, ModelLayer>();
		layers.put(LayerType.TERRAIN, new ModelLayer(LayerType.TERRAIN, size));
		layers.put(LayerType.VISIBILITY, new ModelLayer(LayerType.VISIBILITY, size));
		layers.put(LayerType.TOKEN, new ModelLayer(LayerType.TOKEN, size));
	}
	
	/**
	 * @return the dimensions of this chunk
	 */
	public Dimension getSize()
	{
		return size;
	}
	
	/**
	 * build a new chunk, with preexisting layers.
	 * @param layers a HashMap containing the layers in this chunk
	 */
	public ModelChunk(Map<LayerType, ModelLayer> layers, Dimension size)
	{
		this.layers = layers;
		this.size = size;
	}
	
	/**
	 * get the layer of a given type
	 * @param type the type of the layer to return
	 * @return the layer of that type, or null if there is no layer of that type.
	 */
	public ModelLayer getLayer(LayerType type)
	{
		return layers.get(type);
	}
	
	/**
	 * @return all layers in the chunk
	 */
	public Map<LayerType, ModelLayer> getAllLayers()
	{
		return layers;
	}
	
	/**
	 * get a version of this chunk for updates, which has null values
	 * for each layer and cell which has not been updated.
	 * @return a ModelChunk containing all elements which need updating
	 */
	public ModelChunk grabUpdate()
	{
		
		// the mask used for which cells will be updated, based on visibility
		boolean[][] visibilityUpdateMask = makeVisibilityMask();
				
		// the layers of this chunk which will be updated
		HashMap<LayerType, ModelLayer> updateLayers = new HashMap<LayerType, ModelLayer>(); 
		boolean atLeastOneUpdatedLayerInChunk = false;
		// iterate through the layers of the chunk
		for (ModelLayer layer : layers.values()) {
			// grab the update of the layer (all cells which are the same are null)
			ModelLayer updateLayer = layer.grabUpdate();
			boolean atLeastOneUpdatedCellInLayer = false;
			/* iterate through the cells of the update, replacing any 
			 * which are not true in the mask with null values. */
			for (int y=0; y<size.height; y++) {
				for (int x=0; x<size.width; x++) {
					if (updateLayer != null) {
						if (!visibilityUpdateMask[x][y]) {
							try {
								updateLayer.maskCell(x,y);
							} catch (UpdateStatusException e) {
								System.out.println(e);
							}
							layer.storeUpdate(x,y);
						}
						/*( if the cell after all this is still not null, this layer
						 * has at least one updated cell. */
						if (updateLayer.getCell(x,y) != null) {
							atLeastOneUpdatedCellInLayer = true;
						}
					}
				}
			}
			/* put the masked update layer into updateLayers, if there's
			 * anything to update. */
			if (atLeastOneUpdatedCellInLayer) {
				updateLayers.put(updateLayer.getType(), updateLayer);
				atLeastOneUpdatedLayerInChunk = true;
			}
		}
		// return a new ModelChunk containing updateLayers
		if (atLeastOneUpdatedLayerInChunk) {
			return new ModelChunk(updateLayers, size);
		} else {
			return null;
		}
	}
	
	/**
	 * apply an update to this chunk
	 * @param updateChunk the chunk to be used in updating this one
	 * @throws IllegalArgumentException if the update is a different size to this chunk
	 * @throws UpdateStatesException if the update has any layer which this chunk does not
	 */
	public void applyUpdate(ModelChunk updateChunk)
		throws UpdateStatusException
	{
		if (updateChunk != null) {
			// check that the sizes of the update chunk and this one match
			if (! updateChunk.getSize().equals(this.getSize())) {
				throw new IllegalArgumentException("UpdateChunk Dim: " + updateChunk.getSize() +
												"\nattempted to updateChunk dim: " +
												this.getSize());
			}
			// check that the update doesn't contain any layers which this chunk doesn't contain
			for (LayerType updateLayerType : updateChunk.getAllLayers().keySet()) {
				if (getLayer(updateLayerType) == null) {
					throw new UpdateStatusException("Update chunk " + toString() + 
										" contains layer of type " + updateLayerType.name());
				}
			}
			
			for (ModelLayer updateLayer : updateChunk.getAllLayers().values()) {
				getLayer(updateLayer.getType()).applyUpdate(updateLayer);
			}
		}
	}
	
	/**
	 * make the visibility mask for layer updates
	 */
	private boolean[][] makeVisibilityMask()
	{
		// the mask used for which cells will be updated, based on visibility
		boolean[][] visibilityMask = new boolean[size.width][size.height];
		
		// the actual visibility layer of this chunk
		ModelLayer visibilityLayer = getLayer(LayerType.VISIBILITY);
		
		// loop through the cells of the chunk
		for (int y=0; y<size.height; y++) {
			for (int x=0; x<size.width; x++) {
				/* make the mask. Any cell which is currently visible, or in which the
				 * visibility layer itself is due an update gets a true value in the mask */
				VisibilityCell visibility = (VisibilityCell) visibilityLayer.getCell(x,y);
				visibilityMask[x][y] = 
					(visibility.isCurrentlyVisible() ||
					 visibility.hasBeenUpdated());
			}
		}
		
		return visibilityMask;
	}
	
}
